5.4 并发与同步:锁、条件变量与无锁编程
Go
语言的并发特性是其一大亮点,通过Goroutine
实现并发编程,可以大幅提高程序的执行效率。
在并发编程中,如何确保多个Goroutine
安全地访问共享资源是一个重要的问题。
本节将介绍几种常用的同步机制,包括锁、条件变量,以及无锁编程。
本节代码存放目录为 lesson16
互斥锁
互斥锁(sync.Mutex
)是一种用于保护共享资源的机制,确保同一时间只有一个 Goroutine
能够访问特定的代码块或资源。
通过锁住共享资源,可以防止数据竞争和不一致的问题。简单的来说就是将数据增加一个标识,使用的时候标识更新为1
,那么其他的协程就不可以使用。
以下是一个使用互斥锁的简单示例:
var (
mu sync.Mutex
counter int
)
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
在上面的代码中,increment
函数使用了mu.Lock()
和mu.Unlock()
来确保counter++
操作是并发安全的。
尽管互斥锁能够有效保护共享资源,但在高并发场景下,锁的争用可能会导致性能瓶颈,甚至引发死锁。
条件变量
条件变量(sync.Cond
)允许Goroutine
等待某个条件的满足,并在条件满足后继续执行。它常与互斥锁配合使用,用于实现更复杂的同步逻辑。
以下是一个使用条件变量的示例:
var (
muC sync.Mutex
cond = sync.NewCond(&muC)
ready = false
wg sync.WaitGroup
)
func waitCondition() {
defer wg.Done()
cond.L.Lock()
for !ready {
cond.Wait()
}
// 执行其他操作
fmt.Println("Cond unlock")
cond.L.Unlock()
}
func signalCondition() {
defer wg.Done()
fmt.Println("signalCondition")
cond.L.Lock()
ready = true
cond.Signal() // 唤醒一个等待的 Goroutine
cond.L.Unlock()
}
wg.Add(2)
go waitCondition()
time.Sleep(time.Duration(2) * time.Second)
go signalCondition()
wg.Wait()
结果输出如下所示:
signalCondition
Cond unlock
在上面的代码中,waitCondition
函数等待ready
变量变为 true
。
一旦signalCondition
函数将ready
设置为true
并调用cond.Signal()
,等待的Goroutine
就会被唤醒。
这种方式在一些流程控制上还是比较好用的,比如达到某些条件执行下一步存储日志等。
无锁编程
无锁编程通过使用原子操作来避免锁的开销,从而提高并发性能。Go 语言提供了sync/atomic
包,支持无锁的原子操作。
以下是一个使用原子操作的简单示例:
var (
counterAuto int64
)
func incrementAuto() {
atomic.AddInt64(&counterAuto, 1)
}
在这个例子中,atomic.AddInt64
提供了一种无锁的方式来安全地增加counter
的值。
无锁编程能够显著提高性能,但它的实现通常比使用锁更加复杂,并且可能难以调试。
因此,只有在性能瓶颈明显且锁的开销较大时,才考虑使用无锁编程。
小结
在Go
语言的并发编程中,选择合适的同步机制至关重要。互斥锁、条件变量和无锁编程各有优缺点,我们可以根据具体场景选择最合适的方式,以确保程序的安全性和性能。